Config: Add option to skip all RAW images when indexing #2227
This commit is contained in:
parent
038a78c828
commit
9134c79f4c
58 changed files with 1090 additions and 757 deletions
|
@ -1,8 +1,7 @@
|
|||
version: '3.5'
|
||||
|
||||
## Continuous Integration (CI) Test Environment
|
||||
services:
|
||||
## App Dev Container
|
||||
## Continuous Integration (CI) Environment
|
||||
## Docs: https://docs.photoprism.app/developer-guide/
|
||||
photoprism:
|
||||
build: .
|
||||
|
@ -43,7 +42,7 @@ services:
|
|||
PHOTOPRISM_ORIGINALS_PATH: "/go/src/github.com/photoprism/photoprism/storage/originals"
|
||||
PHOTOPRISM_IMPORT_PATH: "/go/src/github.com/photoprism/photoprism/storage/import"
|
||||
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_SETTINGS: "false" # disables settings UI and API
|
||||
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_DETECT_NSFW: "false" # flag photos as private that MAY be offensive (requires TensorFlow)
|
||||
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_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)
|
||||
|
@ -158,6 +157,10 @@ services:
|
|||
MARIADB_PASSWORD: "photoprism"
|
||||
MARIADB_ROOT_PASSWORD: "photoprism"
|
||||
|
||||
## Dummy OpenID Connect Provider
|
||||
dummy-oidc:
|
||||
image: photoprism/dummy-oidc:220405
|
||||
|
||||
## Dummy WebDAV Server
|
||||
dummy-webdav:
|
||||
image: photoprism/dummy-webdav:20211109
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
version: '3.5'
|
||||
|
||||
## Latest Stable Release for QA
|
||||
services:
|
||||
## App Server
|
||||
## Stable Release
|
||||
## Docs: https://docs.photoprism.org/
|
||||
photoprism-latest:
|
||||
image: photoprism/photoprism:latest
|
||||
|
@ -43,7 +42,7 @@ services:
|
|||
PHOTOPRISM_DATABASE_USER: "photoprism_latest"
|
||||
PHOTOPRISM_DATABASE_PASSWORD: "photoprism_latest"
|
||||
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_SETTINGS: "false" # disables settings UI and API
|
||||
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_DETECT_NSFW: "false" # flag photos as private that MAY be offensive (requires TensorFlow)
|
||||
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_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)
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
version: '3.5'
|
||||
|
||||
## MariaDB Server Versions for Development & Testing
|
||||
# Additional MariaDB and MySQL versions for testing compatibility
|
||||
|
||||
services:
|
||||
## MariaDB 10.8 Database Server
|
||||
## 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:
|
||||
# https://github.com/photoprism/photoprism/issues/47
|
||||
|
||||
## Development Environment with
|
||||
## - App Dev Container
|
||||
## - PostgreSQL Database Server
|
||||
## - and Dummy Services
|
||||
services:
|
||||
## App Dev Container
|
||||
## PhotoPrism Development Environment (PostgresSQL)
|
||||
## Docs: https://docs.photoprism.app/developer-guide/
|
||||
photoprism:
|
||||
build: .
|
||||
|
@ -54,7 +50,7 @@ services:
|
|||
PHOTOPRISM_ORIGINALS_PATH: "/go/src/github.com/photoprism/photoprism/storage/originals"
|
||||
PHOTOPRISM_IMPORT_PATH: "/go/src/github.com/photoprism/photoprism/storage/import"
|
||||
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_SETTINGS: "false" # disables settings UI and API
|
||||
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_DETECT_NSFW: "false" # flag photos as private that MAY be offensive (requires TensorFlow)
|
||||
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_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)
|
||||
|
|
|
@ -1,32 +1,7 @@
|
|||
version: '3.5'
|
||||
|
||||
## Development Environment with
|
||||
## - HTTPS Reverse Proxy
|
||||
## - App Dev Container
|
||||
## - MariaDB Database Server
|
||||
## - Keycloak OpenID Connect Provider
|
||||
## - and Dummy Services
|
||||
services:
|
||||
## Traefik HTTPS Reverse Proxy
|
||||
## 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
|
||||
## PhotoPrism Development Environment (MariaDB)
|
||||
## Docs: https://docs.photoprism.org/developer-guide/
|
||||
photoprism:
|
||||
build: .
|
||||
|
@ -85,15 +60,16 @@ services:
|
|||
PHOTOPRISM_ORIGINALS_PATH: "/go/src/github.com/photoprism/photoprism/storage/originals"
|
||||
PHOTOPRISM_IMPORT_PATH: "/go/src/github.com/photoprism/photoprism/storage/import"
|
||||
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_SETTINGS: "false" # disables settings UI and API
|
||||
PHOTOPRISM_DISABLE_PLACES: "false" # disables reverse geocoding and maps
|
||||
PHOTOPRISM_DISABLE_EXIFTOOL: "false" # disables creating JSON metadata sidecar files with ExifTool
|
||||
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_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_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)
|
||||
|
@ -146,9 +122,28 @@ services:
|
|||
MARIADB_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
|
||||
## Admin Account: admin / photoprism
|
||||
## User Account: user / photoprism
|
||||
## Login: user / photoprism
|
||||
## Admin: admin / photoprism
|
||||
keycloak:
|
||||
image: quay.io/keycloak/keycloak:17.0.1
|
||||
command: "start-dev" # development mode, do not use this in production!
|
||||
|
@ -174,6 +169,18 @@ services:
|
|||
KC_DB_USERNAME: "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:
|
||||
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=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
|
||||
volumes:
|
||||
go-mod:
|
||||
|
|
|
@ -75,8 +75,9 @@ services:
|
|||
PHOTOPRISM_DISABLE_TENSORFLOW: "false" # disables all features depending on TensorFlow
|
||||
PHOTOPRISM_DISABLE_FACES: "false" # disables facial recognition
|
||||
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_RAW_PRESETS: "false" # enables RAW file converter presets (may reduce performance)
|
||||
PHOTOPRISM_DETECT_NSFW: "false" # flag photos as private 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
|
||||
|
|
|
@ -68,8 +68,11 @@ services:
|
|||
PHOTOPRISM_DISABLE_WEBDAV: "false" # disables built-in WebDAV server
|
||||
PHOTOPRISM_DISABLE_SETTINGS: "false" # disables Settings in Web UI
|
||||
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_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_UPLOAD_NSFW: "true" # allows uploads that MAY be offensive
|
||||
# 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_FACES: "false" # disables facial recognition
|
||||
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_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_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
|
||||
|
|
|
@ -66,8 +66,9 @@ services:
|
|||
PHOTOPRISM_DISABLE_TENSORFLOW: "false" # disables all features depending on TensorFlow
|
||||
PHOTOPRISM_DISABLE_FACES: "false" # disables facial recognition
|
||||
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_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_UPLOAD_NSFW: "true" # allows uploads that MAY be offensive
|
||||
# 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_FACES: "false" # disables facial recognition
|
||||
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_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_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
|
||||
|
|
|
@ -68,8 +68,9 @@ services:
|
|||
PHOTOPRISM_DISABLE_TENSORFLOW: "false" # disables all features depending on TensorFlow
|
||||
PHOTOPRISM_DISABLE_FACES: "false" # disables facial recognition
|
||||
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_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_UPLOAD_NSFW: "true" # allows uploads that MAY be offensive
|
||||
# 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_FACES: "false" # disables facial recognition
|
||||
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_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_UPLOAD_NSFW: "true" # allows uploads that MAY be offensive
|
||||
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_FACES: "false" # disables facial recognition
|
||||
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_DETECT_NSFW: "false" # flag photos as private that MAY be offensive (requires TensorFlow)
|
||||
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": {
|
||||
"version": "1.4.103",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.103.tgz",
|
||||
"integrity": "sha512-c/uKWR1Z/W30Wy/sx3dkZoj4BijbXX85QKWu9jJfjho3LBAXNEGAEW3oWiGb+dotA6C6BzCTxL2/aLes7jlUeg=="
|
||||
"version": "1.4.104",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.104.tgz",
|
||||
"integrity": "sha512-2kjoAyiG7uMyGRM9mx25s3HAzmQG2ayuYXxsFmYugHSDcwxREgLtscZvbL1JcW9S/OemeQ3f/SG6JhDwpnCclQ=="
|
||||
},
|
||||
"node_modules/emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
|
@ -4937,23 +4937,23 @@
|
|||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-import": {
|
||||
"version": "2.25.4",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.25.4.tgz",
|
||||
"integrity": "sha512-/KJBASVFxpu0xg1kIBn9AUa8hQVnszpwgE7Ld0lKAlx7Ie87yzEzCgSkekt+le/YVhiaosO4Y14GDAOc41nfxA==",
|
||||
"version": "2.26.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.26.0.tgz",
|
||||
"integrity": "sha512-hYfi3FXaM8WPLf4S1cikh/r4IxnO6zrhZbEGz2b660EJRbuxgpDS5gkCuYgGWg2xxh2rBuIr4Pvhve/7c31koA==",
|
||||
"dependencies": {
|
||||
"array-includes": "^3.1.4",
|
||||
"array.prototype.flat": "^1.2.5",
|
||||
"debug": "^2.6.9",
|
||||
"doctrine": "^2.1.0",
|
||||
"eslint-import-resolver-node": "^0.3.6",
|
||||
"eslint-module-utils": "^2.7.2",
|
||||
"eslint-module-utils": "^2.7.3",
|
||||
"has": "^1.0.3",
|
||||
"is-core-module": "^2.8.0",
|
||||
"is-core-module": "^2.8.1",
|
||||
"is-glob": "^4.0.3",
|
||||
"minimatch": "^3.0.4",
|
||||
"minimatch": "^3.1.2",
|
||||
"object.values": "^1.1.5",
|
||||
"resolve": "^1.20.0",
|
||||
"tsconfig-paths": "^3.12.0"
|
||||
"resolve": "^1.22.0",
|
||||
"tsconfig-paths": "^3.14.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
|
@ -5156,9 +5156,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-vue": {
|
||||
"version": "8.5.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-8.5.0.tgz",
|
||||
"integrity": "sha512-i1uHCTAKOoEj12RDvdtONWrGzjFm/djkzqfhmQ0d6M/W8KM81mhswd/z+iTZ0jCpdUedW3YRgcVfQ37/J4zoYQ==",
|
||||
"version": "8.6.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-8.6.0.tgz",
|
||||
"integrity": "sha512-abXiF2J18n/7ZPy9foSlJyouKf54IqpKlNvNmzhM93N0zs3QUxZG/oBd3tVPOJTKg7SlhBUtPxugpqzNbgGpQQ==",
|
||||
"dependencies": {
|
||||
"eslint-utils": "^3.0.0",
|
||||
"natural-compare": "^1.4.0",
|
||||
|
@ -6037,19 +6037,19 @@
|
|||
"integrity": "sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg=="
|
||||
},
|
||||
"node_modules/flow-parser": {
|
||||
"version": "0.175.0",
|
||||
"resolved": "https://registry.npmjs.org/flow-parser/-/flow-parser-0.175.0.tgz",
|
||||
"integrity": "sha512-9XG5JGOjhODF+OQF5ufCw8XiGi+8B46scjr3Q49JxN7IDRdT2W+1AOuvKKd6j766/5E7qSuCn/dsq1y3hihntg==",
|
||||
"version": "0.175.1",
|
||||
"resolved": "https://registry.npmjs.org/flow-parser/-/flow-parser-0.175.1.tgz",
|
||||
"integrity": "sha512-gYes5/nxeLYiu02MMb+WH4KaOIYrVcTVIuV9M4aP/4hqJ+zULxxS/In+WEj/tEBsQ+8/wSHo9IDWKQL1FhrLmA==",
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/flow-remove-types": {
|
||||
"version": "2.175.0",
|
||||
"resolved": "https://registry.npmjs.org/flow-remove-types/-/flow-remove-types-2.175.0.tgz",
|
||||
"integrity": "sha512-5DKqnBquIdg6KwwH4VtHw+eHw+uCrnhVCTi32q9wMWGjbe2FnpJTDbODKIQyMYowCGTb8jTs0Kqk4Hb66kY1Mg==",
|
||||
"version": "2.175.1",
|
||||
"resolved": "https://registry.npmjs.org/flow-remove-types/-/flow-remove-types-2.175.1.tgz",
|
||||
"integrity": "sha512-malB3a9t7zEi5TMBbQlSZ47vqH673aiN+a34Xlv6/q3fJwyaXYlIz2wa9tMa3h5kM26aH7dFRS0GeBbfV1C5IQ==",
|
||||
"dependencies": {
|
||||
"flow-parser": "^0.175.0",
|
||||
"flow-parser": "^0.175.1",
|
||||
"pirates": "^3.0.2",
|
||||
"vlq": "^0.2.1"
|
||||
},
|
||||
|
@ -6320,9 +6320,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/graceful-fs": {
|
||||
"version": "4.2.9",
|
||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.9.tgz",
|
||||
"integrity": "sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ=="
|
||||
"version": "4.2.10",
|
||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz",
|
||||
"integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA=="
|
||||
},
|
||||
"node_modules/growl": {
|
||||
"version": "1.10.5",
|
||||
|
@ -9028,15 +9028,19 @@
|
|||
}
|
||||
},
|
||||
"node_modules/postcss-custom-properties": {
|
||||
"version": "12.1.5",
|
||||
"resolved": "https://registry.npmjs.org/postcss-custom-properties/-/postcss-custom-properties-12.1.5.tgz",
|
||||
"integrity": "sha512-FHbbB/hRo/7cxLGkc2NS7cDRIDN1oFqQnUKBiyh4b/gwk8DD8udvmRDpUhEK836kB8ggUCieHVOvZDnF9XhI3g==",
|
||||
"version": "12.1.6",
|
||||
"resolved": "https://registry.npmjs.org/postcss-custom-properties/-/postcss-custom-properties-12.1.6.tgz",
|
||||
"integrity": "sha512-QEnQkDkb+J+j2bfJisJJpTAFL+lUFl66rUNvnjPBIvRbZACLG4Eu5bmBCIY4FJCqhwsfbBpmJUyb3FcR/31lAg==",
|
||||
"dependencies": {
|
||||
"postcss-value-parser": "^4.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^12 || ^14 || >=16"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/csstools"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"postcss": "^8.4"
|
||||
}
|
||||
|
@ -11260,9 +11264,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/supercluster": {
|
||||
"version": "7.1.4",
|
||||
"resolved": "https://registry.npmjs.org/supercluster/-/supercluster-7.1.4.tgz",
|
||||
"integrity": "sha512-GhKkRM1jMR6WUwGPw05fs66pOFWhf59lXq+Q3J3SxPvhNcmgOtLRV6aVQPMRsmXdpaeFJGivt+t7QXUPL3ff4g==",
|
||||
"version": "7.1.5",
|
||||
"resolved": "https://registry.npmjs.org/supercluster/-/supercluster-7.1.5.tgz",
|
||||
"integrity": "sha512-EulshI3pGUM66o6ZdH3ReiFcvHpM3vAigyK+vcxdjpJyEbIIrtbmBdY23mGgnI24uXiGFvrGq9Gkum/8U7vJWg==",
|
||||
"dependencies": {
|
||||
"kdbush": "^3.0.0"
|
||||
}
|
||||
|
@ -15911,9 +15915,9 @@
|
|||
}
|
||||
},
|
||||
"electron-to-chromium": {
|
||||
"version": "1.4.103",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.103.tgz",
|
||||
"integrity": "sha512-c/uKWR1Z/W30Wy/sx3dkZoj4BijbXX85QKWu9jJfjho3LBAXNEGAEW3oWiGb+dotA6C6BzCTxL2/aLes7jlUeg=="
|
||||
"version": "1.4.104",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.104.tgz",
|
||||
"integrity": "sha512-2kjoAyiG7uMyGRM9mx25s3HAzmQG2ayuYXxsFmYugHSDcwxREgLtscZvbL1JcW9S/OemeQ3f/SG6JhDwpnCclQ=="
|
||||
},
|
||||
"emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
|
@ -16447,23 +16451,23 @@
|
|||
}
|
||||
},
|
||||
"eslint-plugin-import": {
|
||||
"version": "2.25.4",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.25.4.tgz",
|
||||
"integrity": "sha512-/KJBASVFxpu0xg1kIBn9AUa8hQVnszpwgE7Ld0lKAlx7Ie87yzEzCgSkekt+le/YVhiaosO4Y14GDAOc41nfxA==",
|
||||
"version": "2.26.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.26.0.tgz",
|
||||
"integrity": "sha512-hYfi3FXaM8WPLf4S1cikh/r4IxnO6zrhZbEGz2b660EJRbuxgpDS5gkCuYgGWg2xxh2rBuIr4Pvhve/7c31koA==",
|
||||
"requires": {
|
||||
"array-includes": "^3.1.4",
|
||||
"array.prototype.flat": "^1.2.5",
|
||||
"debug": "^2.6.9",
|
||||
"doctrine": "^2.1.0",
|
||||
"eslint-import-resolver-node": "^0.3.6",
|
||||
"eslint-module-utils": "^2.7.2",
|
||||
"eslint-module-utils": "^2.7.3",
|
||||
"has": "^1.0.3",
|
||||
"is-core-module": "^2.8.0",
|
||||
"is-core-module": "^2.8.1",
|
||||
"is-glob": "^4.0.3",
|
||||
"minimatch": "^3.0.4",
|
||||
"minimatch": "^3.1.2",
|
||||
"object.values": "^1.1.5",
|
||||
"resolve": "^1.20.0",
|
||||
"tsconfig-paths": "^3.12.0"
|
||||
"resolve": "^1.22.0",
|
||||
"tsconfig-paths": "^3.14.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"debug": {
|
||||
|
@ -16604,9 +16608,9 @@
|
|||
"requires": {}
|
||||
},
|
||||
"eslint-plugin-vue": {
|
||||
"version": "8.5.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-8.5.0.tgz",
|
||||
"integrity": "sha512-i1uHCTAKOoEj12RDvdtONWrGzjFm/djkzqfhmQ0d6M/W8KM81mhswd/z+iTZ0jCpdUedW3YRgcVfQ37/J4zoYQ==",
|
||||
"version": "8.6.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-vue/-/eslint-plugin-vue-8.6.0.tgz",
|
||||
"integrity": "sha512-abXiF2J18n/7ZPy9foSlJyouKf54IqpKlNvNmzhM93N0zs3QUxZG/oBd3tVPOJTKg7SlhBUtPxugpqzNbgGpQQ==",
|
||||
"requires": {
|
||||
"eslint-utils": "^3.0.0",
|
||||
"natural-compare": "^1.4.0",
|
||||
|
@ -17140,16 +17144,16 @@
|
|||
"integrity": "sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg=="
|
||||
},
|
||||
"flow-parser": {
|
||||
"version": "0.175.0",
|
||||
"resolved": "https://registry.npmjs.org/flow-parser/-/flow-parser-0.175.0.tgz",
|
||||
"integrity": "sha512-9XG5JGOjhODF+OQF5ufCw8XiGi+8B46scjr3Q49JxN7IDRdT2W+1AOuvKKd6j766/5E7qSuCn/dsq1y3hihntg=="
|
||||
"version": "0.175.1",
|
||||
"resolved": "https://registry.npmjs.org/flow-parser/-/flow-parser-0.175.1.tgz",
|
||||
"integrity": "sha512-gYes5/nxeLYiu02MMb+WH4KaOIYrVcTVIuV9M4aP/4hqJ+zULxxS/In+WEj/tEBsQ+8/wSHo9IDWKQL1FhrLmA=="
|
||||
},
|
||||
"flow-remove-types": {
|
||||
"version": "2.175.0",
|
||||
"resolved": "https://registry.npmjs.org/flow-remove-types/-/flow-remove-types-2.175.0.tgz",
|
||||
"integrity": "sha512-5DKqnBquIdg6KwwH4VtHw+eHw+uCrnhVCTi32q9wMWGjbe2FnpJTDbODKIQyMYowCGTb8jTs0Kqk4Hb66kY1Mg==",
|
||||
"version": "2.175.1",
|
||||
"resolved": "https://registry.npmjs.org/flow-remove-types/-/flow-remove-types-2.175.1.tgz",
|
||||
"integrity": "sha512-malB3a9t7zEi5TMBbQlSZ47vqH673aiN+a34Xlv6/q3fJwyaXYlIz2wa9tMa3h5kM26aH7dFRS0GeBbfV1C5IQ==",
|
||||
"requires": {
|
||||
"flow-parser": "^0.175.0",
|
||||
"flow-parser": "^0.175.1",
|
||||
"pirates": "^3.0.2",
|
||||
"vlq": "^0.2.1"
|
||||
},
|
||||
|
@ -17329,9 +17333,9 @@
|
|||
"integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA=="
|
||||
},
|
||||
"graceful-fs": {
|
||||
"version": "4.2.9",
|
||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.9.tgz",
|
||||
"integrity": "sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ=="
|
||||
"version": "4.2.10",
|
||||
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz",
|
||||
"integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA=="
|
||||
},
|
||||
"growl": {
|
||||
"version": "1.10.5",
|
||||
|
@ -19263,9 +19267,9 @@
|
|||
"requires": {}
|
||||
},
|
||||
"postcss-custom-properties": {
|
||||
"version": "12.1.5",
|
||||
"resolved": "https://registry.npmjs.org/postcss-custom-properties/-/postcss-custom-properties-12.1.5.tgz",
|
||||
"integrity": "sha512-FHbbB/hRo/7cxLGkc2NS7cDRIDN1oFqQnUKBiyh4b/gwk8DD8udvmRDpUhEK836kB8ggUCieHVOvZDnF9XhI3g==",
|
||||
"version": "12.1.6",
|
||||
"resolved": "https://registry.npmjs.org/postcss-custom-properties/-/postcss-custom-properties-12.1.6.tgz",
|
||||
"integrity": "sha512-QEnQkDkb+J+j2bfJisJJpTAFL+lUFl66rUNvnjPBIvRbZACLG4Eu5bmBCIY4FJCqhwsfbBpmJUyb3FcR/31lAg==",
|
||||
"requires": {
|
||||
"postcss-value-parser": "^4.2.0"
|
||||
}
|
||||
|
@ -20855,9 +20859,9 @@
|
|||
}
|
||||
},
|
||||
"supercluster": {
|
||||
"version": "7.1.4",
|
||||
"resolved": "https://registry.npmjs.org/supercluster/-/supercluster-7.1.4.tgz",
|
||||
"integrity": "sha512-GhKkRM1jMR6WUwGPw05fs66pOFWhf59lXq+Q3J3SxPvhNcmgOtLRV6aVQPMRsmXdpaeFJGivt+t7QXUPL3ff4g==",
|
||||
"version": "7.1.5",
|
||||
"resolved": "https://registry.npmjs.org/supercluster/-/supercluster-7.1.5.tgz",
|
||||
"integrity": "sha512-EulshI3pGUM66o6ZdH3ReiFcvHpM3vAigyK+vcxdjpJyEbIIrtbmBdY23mGgnI24uXiGFvrGq9Gkum/8U7vJWg==",
|
||||
"requires": {
|
||||
"kdbush": "^3.0.0"
|
||||
}
|
||||
|
|
|
@ -44,6 +44,7 @@ export class ConfigOptions extends Model {
|
|||
DisableDarktable: config.values.disable.darktable,
|
||||
DisableRawtherapee: config.values.disable.rawtherapee,
|
||||
DisableSips: config.values.disable.sips,
|
||||
DisableRaw: config.values.disable.raw,
|
||||
DisableHeifConvert: config.values.disable.heifconvert,
|
||||
DisableFFmpeg: config.values.disable.ffmpeg,
|
||||
DisableTensorFlow: config.values.disable.tensorflow,
|
||||
|
|
|
@ -247,7 +247,7 @@
|
|||
<v-flex xs12 sm4 class="px-2 pb-2 pt-2">
|
||||
<v-checkbox
|
||||
v-model="settings.RawPresets"
|
||||
:disabled="busy"
|
||||
:disabled="busy || settings.DisableRaw"
|
||||
class="ma-0 pa-0"
|
||||
color="secondary-dark"
|
||||
:label="$gettext('Use Presets')"
|
||||
|
@ -262,7 +262,7 @@
|
|||
<v-flex xs12 sm4 class="px-2 pb-2 pt-2">
|
||||
<v-checkbox
|
||||
v-model="settings.DisableDarktable"
|
||||
:disabled="busy"
|
||||
:disabled="busy || settings.DisableRaw"
|
||||
class="ma-0 pa-0 input-private"
|
||||
color="secondary-dark"
|
||||
:label="$gettext('Disable Darktable')"
|
||||
|
@ -277,7 +277,7 @@
|
|||
<v-flex xs12 sm4 class="px-2 pb-2 pt-2">
|
||||
<v-checkbox
|
||||
v-model="settings.DisableRawtherapee"
|
||||
:disabled="busy"
|
||||
:disabled="busy || settings.DisableRaw"
|
||||
class="ma-0 pa-0 input-private"
|
||||
color="secondary-dark"
|
||||
:label="$gettext('Disable RawTherapee')"
|
||||
|
|
|
@ -115,7 +115,7 @@ func AlbumCover(router *gin.RouterGroup) {
|
|||
c.Data(http.StatusOK, "image/svg+xml", albumIconSvg)
|
||||
return
|
||||
} 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)
|
||||
return
|
||||
}
|
||||
|
@ -229,7 +229,7 @@ func LabelCover(router *gin.RouterGroup) {
|
|||
c.Data(http.StatusOK, "image/svg+xml", labelIconSvg)
|
||||
return
|
||||
} 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)
|
||||
return
|
||||
}
|
||||
|
|
|
@ -124,7 +124,7 @@ func FolderCover(router *gin.RouterGroup) {
|
|||
c.Data(http.StatusOK, "image/svg+xml", folderIconSvg)
|
||||
return
|
||||
} 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)
|
||||
return
|
||||
}
|
||||
|
|
|
@ -193,7 +193,7 @@ func GetThumb(router *gin.RouterGroup) {
|
|||
c.Data(http.StatusOK, "image/svg+xml", brokenIconSvg)
|
||||
return
|
||||
} 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)
|
||||
return
|
||||
}
|
||||
|
|
|
@ -3,14 +3,13 @@ package api
|
|||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/photoprism/photoprism/pkg/sanitize"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/service"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/photoprism"
|
||||
"github.com/photoprism/photoprism/internal/query"
|
||||
"github.com/photoprism/photoprism/internal/service"
|
||||
"github.com/photoprism/photoprism/internal/video"
|
||||
"github.com/photoprism/photoprism/pkg/sanitize"
|
||||
)
|
||||
|
||||
// GetVideo streams videos.
|
||||
|
@ -75,7 +74,7 @@ func GetVideo(router *gin.RouterGroup) {
|
|||
} else if f.FileCodec != string(videoType.Codec) {
|
||||
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))
|
||||
c.Data(http.StatusOK, "image/svg+xml", videoIconSvg)
|
||||
return
|
||||
|
|
|
@ -24,7 +24,7 @@ func configAction(ctx *cli.Context) error {
|
|||
|
||||
dbDriver := conf.DatabaseDriver()
|
||||
|
||||
fmt.Printf("%-25s Value\n", "Name")
|
||||
fmt.Printf("%-25s VALUE\n", "NAME")
|
||||
|
||||
// Flags.
|
||||
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-faces", conf.DisableFaces())
|
||||
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-rawtherapee", conf.DisableRawtherapee())
|
||||
fmt.Printf("%-25s %t\n", "disable-sips", conf.DisableSips())
|
||||
fmt.Printf("%-25s %t\n", "disable-heifconvert", conf.DisableHeifConvert())
|
||||
fmt.Printf("%-25s %t\n", "disable-ffmpeg", conf.DisableFFmpeg())
|
||||
fmt.Printf("%-25s %t\n", "disable-exiftool", conf.DisableExifTool())
|
||||
fmt.Printf("%-25s %t\n", "disable-raw", conf.DisableRaw())
|
||||
|
||||
// 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", "exif-bruteforce", conf.ExifBruteForce())
|
||||
|
||||
// TensorFlow.
|
||||
fmt.Printf("%-25s %t\n", "detect-nsfw", conf.DetectNSFW())
|
||||
|
|
|
@ -23,7 +23,7 @@ func TestConfigCommand(t *testing.T) {
|
|||
}
|
||||
|
||||
// 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, "darktable-cli")
|
||||
assert.Contains(t, output, "originals-path")
|
||||
|
|
|
@ -27,7 +27,7 @@ func (c *Config) CheckPassword(p string) bool {
|
|||
return ap == p
|
||||
}
|
||||
|
||||
// InvalidDownloadToken tests if the token is invalid.
|
||||
// InvalidDownloadToken checks if the token is invalid.
|
||||
func (c *Config) InvalidDownloadToken(t string) bool {
|
||||
return c.DownloadToken() != t
|
||||
}
|
||||
|
@ -41,7 +41,7 @@ func (c *Config) DownloadToken() string {
|
|||
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 {
|
||||
return c.PreviewToken() != t && c.DownloadToken() != t
|
||||
}
|
||||
|
@ -51,6 +51,8 @@ func (c *Config) PreviewToken() string {
|
|||
if c.options.PreviewToken == "" {
|
||||
if c.Public() {
|
||||
c.options.PreviewToken = "public"
|
||||
} else if c.Serial() == "" {
|
||||
return "********"
|
||||
} else {
|
||||
c.options.PreviewToken = c.SerialChecksum()
|
||||
}
|
||||
|
|
|
@ -77,6 +77,7 @@ type ClientDisable struct {
|
|||
Places bool `json:"places"`
|
||||
ExifTool bool `json:"exiftool"`
|
||||
FFmpeg bool `json:"ffmpeg"`
|
||||
Raw bool `json:"raw"`
|
||||
Darktable bool `json:"darktable"`
|
||||
Rawtherapee bool `json:"rawtherapee"`
|
||||
Sips bool `json:"sips"`
|
||||
|
@ -193,6 +194,7 @@ func (c *Config) PublicConfig() ClientConfig {
|
|||
Places: c.DisablePlaces(),
|
||||
ExifTool: true,
|
||||
FFmpeg: true,
|
||||
Raw: true,
|
||||
Darktable: true,
|
||||
Rawtherapee: true,
|
||||
Sips: true,
|
||||
|
@ -264,6 +266,7 @@ func (c *Config) GuestConfig() ClientConfig {
|
|||
Places: c.DisablePlaces(),
|
||||
ExifTool: true,
|
||||
FFmpeg: true,
|
||||
Raw: true,
|
||||
Darktable: true,
|
||||
Rawtherapee: true,
|
||||
Sips: true,
|
||||
|
@ -329,6 +332,7 @@ func (c *Config) UserConfig() ClientConfig {
|
|||
Places: c.DisablePlaces(),
|
||||
ExifTool: c.DisableExifTool(),
|
||||
FFmpeg: c.DisableFFmpeg(),
|
||||
Raw: c.DisableRaw(),
|
||||
Darktable: c.DisableDarktable(),
|
||||
Rawtherapee: c.DisableRawtherapee(),
|
||||
Sips: c.DisableSips(),
|
||||
|
|
|
@ -67,6 +67,9 @@ const MinMem = Gigabyte
|
|||
// RecommendedMem is the recommended amount of system memory.
|
||||
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
|
||||
type Config struct {
|
||||
once sync.Once
|
||||
|
@ -143,7 +146,7 @@ func (c *Config) Unsafe() bool {
|
|||
// Options returns the raw config options.
|
||||
func (c *Config) Options() *Options {
|
||||
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)
|
||||
}
|
||||
|
||||
|
@ -186,7 +189,7 @@ func (c *Config) Init() error {
|
|||
return err
|
||||
}
|
||||
|
||||
if err := c.initStorage(); err != nil {
|
||||
if err := c.initSerial(); err != nil {
|
||||
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))
|
||||
}
|
||||
|
||||
// Check memory requirements.
|
||||
// Exit if less than 128 MB RAM was detected.
|
||||
if TotalMem < 128*Megabyte {
|
||||
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: tensorflow as well as indexing and conversion of RAW files have been disabled automatically")
|
||||
}
|
||||
|
||||
// Show swap info.
|
||||
|
@ -242,28 +249,47 @@ func (c *Config) Init() error {
|
|||
return err
|
||||
}
|
||||
|
||||
// initStorage initializes storage directories with a random serial.
|
||||
func (c *Config) initStorage() error {
|
||||
if c.serial != "" {
|
||||
return nil
|
||||
// readSerial reads and returns the current storage serial.
|
||||
func (c *Config) readSerial() string {
|
||||
storageName := filepath.Join(c.StoragePath(), serialName)
|
||||
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')
|
||||
|
||||
storageName := filepath.Join(c.StoragePath(), serialName)
|
||||
backupName := filepath.Join(c.BackupPath(), serialName)
|
||||
|
||||
if data, err := os.ReadFile(storageName); err == nil && len(data) == 16 {
|
||||
c.serial = string(data)
|
||||
} 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))
|
||||
} else if err := os.WriteFile(storageName, []byte(c.serial), os.ModePerm); err != nil {
|
||||
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)
|
||||
if err = os.WriteFile(storageName, []byte(c.serial), os.ModePerm); err != nil {
|
||||
return fmt.Errorf("could not create %s: %s", storageName, err)
|
||||
}
|
||||
|
||||
if err = os.WriteFile(backupName, []byte(c.serial), os.ModePerm); err != nil {
|
||||
return fmt.Errorf("could not create %s: %s", backupName, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
@ -271,8 +297,8 @@ func (c *Config) initStorage() error {
|
|||
|
||||
// Serial returns the random storage serial.
|
||||
func (c *Config) Serial() string {
|
||||
if err := c.initStorage(); err != nil {
|
||||
log.Errorf("config: %s", err)
|
||||
if c.serial == "" {
|
||||
c.serial = c.readSerial()
|
||||
}
|
||||
|
||||
return c.serial
|
||||
|
@ -419,17 +445,17 @@ func (c *Config) ImprintUrl() string {
|
|||
return c.options.ImprintUrl
|
||||
}
|
||||
|
||||
// Debug tests if debug mode is enabled.
|
||||
// Debug checks if debug mode is enabled.
|
||||
func (c *Config) Debug() bool {
|
||||
return c.options.Debug
|
||||
}
|
||||
|
||||
// Test tests if test mode is enabled.
|
||||
// Test checks if test mode is enabled.
|
||||
func (c *Config) Test() bool {
|
||||
return c.options.Test
|
||||
}
|
||||
|
||||
// Demo tests if demo mode is enabled.
|
||||
// Demo checks if demo mode is enabled.
|
||||
func (c *Config) Demo() bool {
|
||||
return c.options.Demo
|
||||
}
|
||||
|
@ -439,7 +465,7 @@ func (c *Config) Sponsor() bool {
|
|||
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 {
|
||||
if c.Demo() {
|
||||
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 {
|
||||
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 {
|
||||
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 {
|
||||
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 {
|
||||
return c.options.UploadNSFW
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
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 {
|
||||
if c.ReadOnly() || c.Demo() {
|
||||
return true
|
||||
|
@ -9,7 +9,7 @@ func (c *Config) DisableWebDAV() bool {
|
|||
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 {
|
||||
if !c.SidecarWritable() {
|
||||
return true
|
||||
|
@ -18,36 +18,37 @@ func (c *Config) DisableBackups() bool {
|
|||
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 {
|
||||
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 {
|
||||
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 {
|
||||
if !c.SidecarWritable() || c.ExifToolBin() == "" {
|
||||
if c.options.DisableExifTool {
|
||||
return true
|
||||
} else if !c.SidecarWritable() || c.ExifToolBin() == "" {
|
||||
c.options.DisableExifTool = true
|
||||
}
|
||||
|
||||
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 {
|
||||
if LowMem && !c.options.DisableTensorFlow {
|
||||
c.options.DisableTensorFlow = true
|
||||
log.Warnf("config: disabled tensorflow due to memory constraints")
|
||||
}
|
||||
|
||||
return c.options.DisableTensorFlow
|
||||
}
|
||||
|
||||
// DisableFaces tests if facial recognition is disabled.
|
||||
// DisableFaces checks if facial recognition is disabled.
|
||||
func (c *Config) DisableFaces() bool {
|
||||
if c.DisableTensorFlow() || c.options.DisableFaces {
|
||||
return true
|
||||
|
@ -56,7 +57,7 @@ func (c *Config) DisableFaces() bool {
|
|||
return false
|
||||
}
|
||||
|
||||
// DisableClassification tests if image classification is disabled.
|
||||
// DisableClassification checks if image classification is disabled.
|
||||
func (c *Config) DisableClassification() bool {
|
||||
if c.DisableTensorFlow() || c.options.DisableClassification {
|
||||
return true
|
||||
|
@ -65,37 +66,67 @@ func (c *Config) DisableClassification() bool {
|
|||
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 {
|
||||
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 {
|
||||
if LowMem && !c.options.DisableDarktable {
|
||||
if c.DisableRaw() || c.options.DisableDarktable {
|
||||
return true
|
||||
} else if c.DarktableBin() == "" {
|
||||
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 {
|
||||
if LowMem && !c.options.DisableRawtherapee {
|
||||
if c.DisableRaw() || c.options.DisableRawtherapee {
|
||||
return true
|
||||
} else if c.RawtherapeeBin() == "" {
|
||||
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 {
|
||||
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 {
|
||||
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
|
||||
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
|
||||
|
||||
import "github.com/photoprism/photoprism/internal/ffmpeg"
|
||||
|
||||
// FFmpegBin returns the ffmpeg executable file name.
|
||||
func (c *Config) FFmpegBin() string {
|
||||
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 {
|
||||
return !c.DisableFFmpeg()
|
||||
}
|
||||
|
||||
// FFmpegEncoder returns the ffmpeg AVC encoder name.
|
||||
func (c *Config) FFmpegEncoder() string {
|
||||
if c.options.FFmpegEncoder == "" {
|
||||
return "libx264"
|
||||
}
|
||||
|
||||
return c.options.FFmpegEncoder
|
||||
// FFmpegEncoder returns the FFmpeg AVC encoder name.
|
||||
func (c *Config) FFmpegEncoder() ffmpeg.AvcEncoder {
|
||||
return ffmpeg.FindEncoder(c.options.FFmpegEncoder)
|
||||
}
|
||||
|
||||
// FFmpegBitrate returns the ffmpeg bitrate limit in MBit/s.
|
||||
|
|
|
@ -3,15 +3,22 @@ package config
|
|||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/ffmpeg"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestConfig_FFmpegEncoder(t *testing.T) {
|
||||
c := NewConfig(CliTestContext())
|
||||
assert.Equal(t, "libx264", c.FFmpegEncoder())
|
||||
|
||||
c.options.FFmpegEncoder = "testEncoder"
|
||||
assert.Equal(t, "testEncoder", c.FFmpegEncoder())
|
||||
assert.Equal(t, ffmpeg.SoftwareEncoder, c.FFmpegEncoder())
|
||||
c.options.FFmpegEncoder = "nvidia"
|
||||
assert.Equal(t, ffmpeg.NvidiaEncoder, 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) {
|
||||
|
|
|
@ -174,29 +174,9 @@ var GlobalFlags = []cli.Flag{
|
|||
},
|
||||
cli.BoolFlag{
|
||||
Name: "disable-backups",
|
||||
Usage: "disable creating YAML metadata files",
|
||||
Usage: "disable backing up albums and photo metadata to YAML files",
|
||||
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{
|
||||
Name: "disable-tensorflow",
|
||||
Usage: "disable all features depending on TensorFlow",
|
||||
|
@ -223,15 +203,40 @@ var GlobalFlags = []cli.Flag{
|
|||
EnvVar: "PHOTOPRISM_DISABLE_EXIFTOOL",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "exif-bruteforce",
|
||||
Usage: "always perform a brute-force search if no Exif headers were found",
|
||||
EnvVar: "PHOTOPRISM_EXIF_BRUTEFORCE",
|
||||
Name: "disable-heifconvert",
|
||||
Usage: "disable conversion of HEIC/HEIF files",
|
||||
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{
|
||||
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",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "exif-bruteforce",
|
||||
Usage: "always perform a brute-force search if no Exif headers were found",
|
||||
EnvVar: "PHOTOPRISM_EXIF_BRUTEFORCE",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "detect-nsfw",
|
||||
Usage: "flag photos as private that may be offensive (requires TensorFlow)",
|
||||
|
@ -397,6 +402,18 @@ var GlobalFlags = []cli.Flag{
|
|||
Value: "dng,cr3",
|
||||
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{
|
||||
Name: "rawtherapee-bin",
|
||||
Usage: "RawTherapee CLI `COMMAND` for RAW to JPEG conversion",
|
||||
|
@ -417,7 +434,7 @@ var GlobalFlags = []cli.Flag{
|
|||
},
|
||||
cli.StringFlag{
|
||||
Name: "heifconvert-bin",
|
||||
Usage: "HEIC/HEIF image convert `COMMAND`",
|
||||
Usage: "HEIC/HEIF image conversion `COMMAND`",
|
||||
Value: "heif-convert",
|
||||
EnvVar: "PHOTOPRISM_HEIFCONVERT_BIN",
|
||||
},
|
||||
|
@ -463,7 +480,7 @@ var GlobalFlags = []cli.Flag{
|
|||
},
|
||||
cli.StringFlag{
|
||||
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",
|
||||
EnvVar: "PHOTOPRISM_THUMB_COLORSPACE",
|
||||
},
|
||||
|
|
|
@ -11,31 +11,46 @@ import (
|
|||
"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 == "" {
|
||||
result = defaultBin
|
||||
binPath = defaultBin
|
||||
} else {
|
||||
result = configBin
|
||||
binPath = configBin
|
||||
}
|
||||
|
||||
if path, err := exec.LookPath(result); err == nil {
|
||||
result = path
|
||||
// Search.
|
||||
if path, err := exec.LookPath(binPath); err == nil {
|
||||
binPath = path
|
||||
}
|
||||
|
||||
if !fs.FileExists(result) {
|
||||
result = ""
|
||||
// Exists?
|
||||
if !fs.FileExists(binPath) {
|
||||
binPath = ""
|
||||
} else {
|
||||
binPaths[cacheKey] = binPath
|
||||
}
|
||||
|
||||
return result
|
||||
return binPath
|
||||
}
|
||||
|
||||
// CreateDirectories creates directories for storing photos, metadata and cache files.
|
||||
func (c *Config) CreateDirectories() error {
|
||||
createError := func(path string, err error) (result error) {
|
||||
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 {
|
||||
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)
|
||||
|
@ -44,7 +59,7 @@ func (c *Config) CreateDirectories() 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() == "" {
|
||||
|
@ -137,6 +152,16 @@ func (c *Config) CreateDirectories() error {
|
|||
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
|
||||
}
|
||||
|
||||
|
@ -190,7 +215,7 @@ func (c *Config) LogFilename() string {
|
|||
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) {
|
||||
storagePath := c.StoragePath()
|
||||
return fs.CaseInsensitive(storagePath)
|
||||
|
@ -225,12 +250,12 @@ func (c *Config) SidecarPath() string {
|
|||
return c.options.SidecarPath
|
||||
}
|
||||
|
||||
// SidecarPathIsAbs tests if sidecar path is absolute.
|
||||
// SidecarPathIsAbs checks if sidecar path is absolute.
|
||||
func (c *Config) SidecarPathIsAbs() bool {
|
||||
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 {
|
||||
return !c.ReadOnly() || c.SidecarPathIsAbs()
|
||||
}
|
||||
|
|
|
@ -10,12 +10,12 @@ func (c *Config) ExifToolBin() string {
|
|||
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 {
|
||||
return !c.DisableExifTool()
|
||||
}
|
||||
|
||||
// BackupYaml tests if creating YAML files is enabled.
|
||||
// BackupYaml checks if creating YAML files is enabled.
|
||||
func (c *Config) BackupYaml() bool {
|
||||
return !c.DisableBackups()
|
||||
}
|
||||
|
|
|
@ -71,17 +71,18 @@ type Options struct {
|
|||
DisableBackups bool `yaml:"DisableBackups" json:"DisableBackups" flag:"disable-backups"`
|
||||
DisableSettings bool `yaml:"DisableSettings" json:"-" flag:"disable-settings"`
|
||||
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"`
|
||||
DisableFaces bool `yaml:"DisableFaces" json:"DisableFaces" flag:"disable-faces"`
|
||||
DisableClassification bool `yaml:"DisableClassification" json:"DisableClassification" flag:"disable-classification"`
|
||||
DisableFFmpeg bool `yaml:"DisableFFmpeg" json:"DisableFFmpeg" flag:"disable-ffmpeg"`
|
||||
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"`
|
||||
ExifBruteForce bool `yaml:"ExifBruteForce" json:"ExifBruteForce" flag:"exif-bruteforce"`
|
||||
DetectNSFW bool `yaml:"DetectNSFW" json:"DetectNSFW" flag:"detect-nsfw"`
|
||||
UploadNSFW bool `yaml:"UploadNSFW" json:"-" flag:"upload-nsfw"`
|
||||
DefaultTheme string `yaml:"DefaultTheme" json:"DefaultTheme" flag:"default-theme"`
|
||||
|
@ -111,6 +112,8 @@ type Options struct {
|
|||
HttpMode string `yaml:"HttpMode" json:"-" flag:"http-mode"`
|
||||
HttpCompression string `yaml:"HttpCompression" json:"-" flag:"http-compression"`
|
||||
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"`
|
||||
RawtherapeeBin string `yaml:"RawtherapeeBin" json:"-" flag:"rawtherapee-bin"`
|
||||
RawtherapeeBlacklist string `yaml:"RawtherapeeBlacklist" json:"-" flag:"rawtherapee-blacklist"`
|
||||
|
|
|
@ -3,11 +3,14 @@ package config
|
|||
import (
|
||||
"os"
|
||||
"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 {
|
||||
return c.options.RawPresets
|
||||
}
|
||||
|
@ -24,27 +27,49 @@ func (c *Config) DarktableBlacklist() string {
|
|||
|
||||
// DarktableConfigPath returns the darktable config directory.
|
||||
func (c *Config) DarktableConfigPath() string {
|
||||
dir := filepath.Join(c.ConfigPath(), "darktable")
|
||||
if err := os.MkdirAll(dir, os.ModePerm); err != nil {
|
||||
log.Errorf("darktable: cannot create config directory %s, check permissions", sanitize.Log(dir))
|
||||
return c.ConfigPath()
|
||||
if c.options.DarktableConfigPath != "" {
|
||||
return c.options.DarktableConfigPath
|
||||
}
|
||||
return dir
|
||||
|
||||
return filepath.Join(c.ConfigPath(), "darktable")
|
||||
}
|
||||
|
||||
// DarktableCachePath returns the darktable cache directory.
|
||||
func (c *Config) DarktableCachePath() string {
|
||||
dir := filepath.Join(c.CachePath(), "darktable")
|
||||
|
||||
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()
|
||||
if c.options.DarktableCachePath != "" {
|
||||
return c.options.DarktableCachePath
|
||||
}
|
||||
|
||||
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 {
|
||||
return !c.DisableDarktable()
|
||||
}
|
||||
|
@ -59,12 +84,12 @@ func (c *Config) RawtherapeeBlacklist() string {
|
|||
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 {
|
||||
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 {
|
||||
return !c.DisableSips()
|
||||
}
|
||||
|
@ -79,7 +104,7 @@ func (c *Config) HeifConvertBin() string {
|
|||
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 {
|
||||
return !c.DisableHeifConvert()
|
||||
}
|
||||
|
|
|
@ -6,6 +6,12 @@ import (
|
|||
"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) {
|
||||
c := NewConfig(CliTestContext())
|
||||
|
||||
|
@ -64,8 +70,7 @@ func TestConfig_SipsBin(t *testing.T) {
|
|||
|
||||
func TestConfig_SipsEnabled(t *testing.T) {
|
||||
c := NewConfig(CliTestContext())
|
||||
c.options.DisableSips = true
|
||||
assert.False(t, c.SipsEnabled())
|
||||
assert.NotEqual(t, c.DisableSips(), c.SipsEnabled())
|
||||
}
|
||||
|
||||
func TestConfig_HeifConvertBin(t *testing.T) {
|
||||
|
|
|
@ -7,7 +7,7 @@ import (
|
|||
"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 {
|
||||
return c.options.DetachServer
|
||||
}
|
||||
|
@ -53,7 +53,7 @@ func (c *Config) TemplatesPath() string {
|
|||
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 {
|
||||
return fs.FileExists(filepath.Join(c.TemplatesPath(), name))
|
||||
}
|
||||
|
|
|
@ -4,12 +4,12 @@ import (
|
|||
"fmt"
|
||||
"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/pkg/fs"
|
||||
"github.com/photoprism/photoprism/pkg/sanitize"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
// UISettings represents user interface settings.
|
||||
|
@ -172,17 +172,17 @@ func (s *Settings) Propagate() {
|
|||
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 {
|
||||
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 {
|
||||
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 {
|
||||
return s.Stack.Meta
|
||||
}
|
||||
|
@ -235,11 +235,11 @@ func (c *Config) initSettings() {
|
|||
fileName := c.SettingsFile()
|
||||
|
||||
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 {
|
||||
log.Errorf("failed creating %s: %s", fileName, err)
|
||||
log.Errorf("settings: could not create %s (%s)", fileName, err)
|
||||
} else {
|
||||
log.Debugf("config: created %s ", fileName)
|
||||
log.Debugf("settings: saved to %s ", fileName)
|
||||
}
|
||||
|
||||
i18n.SetDir(c.LocalesPath())
|
||||
|
|
|
@ -250,7 +250,7 @@ func FirstOrCreateCell(m *Cell) *Cell {
|
|||
// Keywords returns search keywords for a location.
|
||||
func (m *Cell) Keywords() (result []string) {
|
||||
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
|
||||
}
|
||||
|
||||
|
|
|
@ -54,7 +54,7 @@ func Update(m interface{}, keyNames ...string) (err error) {
|
|||
if res.Error != nil {
|
||||
return err
|
||||
} 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
|
||||
} else if res.RowsAffected == 1 {
|
||||
return nil
|
||||
|
|
|
@ -72,7 +72,7 @@ func (m *Marker) BeforeCreate(scope *gorm.Scope) error {
|
|||
// NewMarker creates a new entity.
|
||||
func NewMarker(file File, area crop.Area, subjUID, markerSrc, markerType string, size, score int) *Marker {
|
||||
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
|
||||
}
|
||||
|
||||
|
|
|
@ -442,7 +442,7 @@ func (m *Photo) IndexKeywords() error {
|
|||
kw := FirstOrCreateKeyword(NewKeyword(w))
|
||||
|
||||
if kw == nil {
|
||||
log.Errorf("index keyword should not be nil - bug?")
|
||||
log.Errorf("index keyword should not be nil - possible bug")
|
||||
continue
|
||||
}
|
||||
|
||||
|
@ -562,7 +562,7 @@ func (m *Photo) AddLabels(labels classify.Labels) {
|
|||
labelEntity := FirstOrCreateLabel(NewLabel(classifyLabel.Title(), classifyLabel.Priority))
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -578,7 +578,7 @@ func (m *Photo) AddLabels(labels classify.Labels) {
|
|||
photoLabel := FirstOrCreatePhotoLabel(NewPhotoLabel(m.ID, labelEntity.ID, classifyLabel.Uncertainty, classifyLabel.Source))
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
|
|
|
@ -87,7 +87,7 @@ func FirstOrCreatePhotoLabel(m *PhotoLabel) *PhotoLabel {
|
|||
// ClassifyLabel returns the label as classify.Label
|
||||
func (m *PhotoLabel) ClassifyLabel() classify.Label {
|
||||
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{}
|
||||
}
|
||||
|
||||
|
|
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
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime/debug"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/karrick/godirwalk"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/config"
|
||||
"github.com/photoprism/photoprism/internal/event"
|
||||
"github.com/photoprism/photoprism/internal/mutex"
|
||||
"github.com/photoprism/photoprism/internal/thumb"
|
||||
"github.com/photoprism/photoprism/pkg/fs"
|
||||
"github.com/photoprism/photoprism/pkg/sanitize"
|
||||
)
|
||||
|
@ -130,242 +122,3 @@ func (c *Convert) Start(path string, force bool) (err error) {
|
|||
|
||||
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"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/event"
|
||||
"github.com/photoprism/photoprism/internal/ffmpeg"
|
||||
|
||||
"github.com/photoprism/photoprism/pkg/fs"
|
||||
"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.
|
||||
func (c *Convert) ToAvc(f *MediaFile, encoderName string) (file *MediaFile, err error) {
|
||||
if n := FFmpegAvcEncoders[encoderName]; n != "" {
|
||||
encoderName = n
|
||||
} else {
|
||||
log.Warnf("convert: unsupported ffmpeg encoder %s", encoderName)
|
||||
encoderName = FFmpegSoftwareEncoder
|
||||
}
|
||||
|
||||
func (c *Convert) ToAvc(f *MediaFile, encoder ffmpeg.AvcEncoder, noMutex, force bool) (file *MediaFile, err error) {
|
||||
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() {
|
||||
|
@ -219,22 +46,30 @@ func (c *Convert) ToAvc(f *MediaFile, encoderName string) (file *MediaFile, err
|
|||
fileName := f.RelName(c.conf.OriginalsPath())
|
||||
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 {
|
||||
log.Error(err)
|
||||
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
|
||||
// Make sure only one convert command runs at a time.
|
||||
if useMutex && !noMutex {
|
||||
c.cmdMutex.Lock()
|
||||
defer c.cmdMutex.Unlock()
|
||||
}
|
||||
|
||||
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.
|
||||
|
@ -250,7 +85,7 @@ func (c *Convert) ToAvc(f *MediaFile, encoderName string) (file *MediaFile, err
|
|||
"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.Trace(cmd.String())
|
||||
|
@ -258,8 +93,6 @@ func (c *Convert) ToAvc(f *MediaFile, encoderName string) (file *MediaFile, err
|
|||
// Run convert command.
|
||||
start := time.Now()
|
||||
if err = cmd.Run(); err != nil {
|
||||
_ = os.Remove(avcName)
|
||||
|
||||
if 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.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 {
|
||||
return c.ToAvc(f, FFmpegSoftwareEncoder)
|
||||
// Remove broken video file.
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// 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"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/photoprism/photoprism/internal/config"
|
||||
"github.com/photoprism/photoprism/pkg/fs"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestConvert_ToAvc(t *testing.T) {
|
||||
|
@ -28,7 +29,7 @@ func TestConvert_ToAvc(t *testing.T) {
|
|||
t.Fatal(err)
|
||||
}
|
||||
|
||||
avcFile, err := convert.ToAvc(mf, "")
|
||||
avcFile, err := convert.ToAvc(mf, "", false, false)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
|
@ -59,7 +60,7 @@ func TestConvert_ToAvc(t *testing.T) {
|
|||
t.Fatal(err)
|
||||
}
|
||||
|
||||
avcFile, err := convert.ToAvc(mf, "")
|
||||
avcFile, err := convert.ToAvc(mf, "", false, false)
|
||||
assert.Error(t, err)
|
||||
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)
|
||||
} else if metaData := job.file.MetaData(); metaData.CodecAvc() {
|
||||
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)
|
||||
}
|
||||
default:
|
||||
|
|
|
@ -72,7 +72,7 @@ func (w *Faces) Cluster(opt FacesOptions) (added entity.Faces, err error) {
|
|||
|
||||
for _, cluster := range results {
|
||||
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() {
|
||||
log.Infof("faces: skipped cluster %s, embedding not distinct enough", f.ID)
|
||||
} 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()
|
||||
indexOpt := NewIndexOptions("/", true, convert, true, false)
|
||||
|
||||
skipRaw := imp.conf.DisableRaw()
|
||||
ignore := fs.NewIgnoreList(fs.IgnoreFile, true, false)
|
||||
|
||||
if err := ignore.Dir(importPath); err != nil {
|
||||
|
@ -156,20 +156,23 @@ func (imp *Import) Start(opt ImportOptions) fs.Done {
|
|||
|
||||
mf, err := NewMediaFile(fileName)
|
||||
|
||||
// Check if file exists and is not empty.
|
||||
if err != nil {
|
||||
log.Warnf("import: %s", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
if mf.FileSize() == 0 {
|
||||
log.Infof("import: skipped empty file %s", sanitize.Log(mf.BaseName()))
|
||||
// Ignore RAW images?
|
||||
if mf.IsRaw() && skipRaw {
|
||||
log.Infof("import: skipped raw %s", sanitize.Log(mf.RootRelName()))
|
||||
return nil
|
||||
}
|
||||
|
||||
// Find related files to import.
|
||||
related, err := mf.RelatedFiles(imp.conf.Settings().StackSequences())
|
||||
|
||||
if err != nil {
|
||||
event.Error(fmt.Sprintf("import: %s", err.Error()))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
@ -126,6 +126,7 @@ func (ind *Index) Start(o IndexOptions) fs.Done {
|
|||
defer ind.files.Done()
|
||||
|
||||
filesIndexed := 0
|
||||
skipRaw := ind.conf.DisableRaw()
|
||||
ignore := fs.NewIgnoreList(fs.IgnoreFile, true, false)
|
||||
|
||||
if err := ignore.Dir(originalsPath); err != nil {
|
||||
|
@ -176,25 +177,28 @@ func (ind *Index) Start(o IndexOptions) fs.Done {
|
|||
|
||||
mf, err := NewMediaFile(fileName)
|
||||
|
||||
// Check if file exists and is not empty.
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
log.Errorf("index: %s", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
if mf.FileSize() == 0 {
|
||||
log.Infof("index: skipped empty file %s", sanitize.Log(mf.BaseName()))
|
||||
// Ignore RAW images?
|
||||
if mf.IsRaw() && skipRaw {
|
||||
log.Infof("index: skipped raw %s", sanitize.Log(mf.RootRelName()))
|
||||
return nil
|
||||
}
|
||||
|
||||
// Skip?
|
||||
if ind.files.Indexed(relName, entity.RootOriginals, mf.modTime, o.Rescan) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Find related files to index.
|
||||
related, err := mf.RelatedFiles(ind.conf.Settings().StackSequences())
|
||||
|
||||
if err != nil {
|
||||
log.Warnf("index: %s", err.Error())
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
@ -23,7 +23,7 @@ import (
|
|||
// MediaFile indexes a single media file.
|
||||
func (ind *Index) MediaFile(m *MediaFile, o IndexOptions, originalName, photoUID string) (result IndexResult) {
|
||||
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)
|
||||
result.Err = err
|
||||
result.Status = IndexFailed
|
||||
|
|
|
@ -74,8 +74,11 @@ func NewMediaFile(fileName string) (*MediaFile, error) {
|
|||
height: -1,
|
||||
}
|
||||
|
||||
if _, _, err := m.Stat(); err != nil {
|
||||
return m, fmt.Errorf("media: %s not found", sanitize.Log(m.BaseName()))
|
||||
// Check if file exists and is not empty.
|
||||
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
|
||||
|
@ -297,6 +300,9 @@ func (m *MediaFile) RelatedFiles(stripSequence bool) (result RelatedFiles, err e
|
|||
sidecarPrefix := Config().SidecarPath() + "/"
|
||||
originalsPrefix := Config().OriginalsPath() + "/"
|
||||
|
||||
// Ignore RAW images?
|
||||
skipRaw := Config().DisableRaw()
|
||||
|
||||
// Replace sidecar with originals path in search prefix.
|
||||
if len(sidecarPrefix) > 1 && sidecarPrefix != originalsPrefix && strings.HasPrefix(prefix, sidecarPrefix) {
|
||||
prefix = strings.Replace(prefix, sidecarPrefix, originalsPrefix, 1)
|
||||
|
@ -326,15 +332,16 @@ func (m *MediaFile) RelatedFiles(stripSequence bool) (result RelatedFiles, err e
|
|||
isHEIF := false
|
||||
|
||||
for _, fileName := range matches {
|
||||
f, err := NewMediaFile(fileName)
|
||||
f, fileErr := NewMediaFile(fileName)
|
||||
|
||||
if err != nil {
|
||||
log.Warnf("media: %s in %s", err, sanitize.Log(filepath.Base(fileName)))
|
||||
if fileErr != nil {
|
||||
log.Warn(fileErr)
|
||||
continue
|
||||
}
|
||||
|
||||
if f.FileSize() == 0 {
|
||||
log.Warnf("media: %s is empty", sanitize.Log(filepath.Base(fileName)))
|
||||
// Ignore RAW images?
|
||||
if f.IsRaw() && skipRaw {
|
||||
log.Debugf("media: skipped related raw file %s", sanitize.Log(f.RootRelName()))
|
||||
continue
|
||||
}
|
||||
|
||||
|
|
|
@ -1742,10 +1742,10 @@ func TestMediaFile_Megapixels(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 {
|
||||
t.Fatal(err)
|
||||
if _, err := NewMediaFile("testdata/2018-04-12 19_24_49.mov"); err != nil {
|
||||
assert.EqualError(t, err, "'testdata/2018-04-12 19_24_49.mov' is empty")
|
||||
} else {
|
||||
assert.Equal(t, 0, f.Megapixels())
|
||||
t.Errorf("error expected")
|
||||
}
|
||||
})
|
||||
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)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue