Config: Add option to skip all RAW images when indexing #2227

This commit is contained in:
Michael Mayer 2022-04-06 17:46:41 +02:00
parent 038a78c828
commit 9134c79f4c
58 changed files with 1090 additions and 757 deletions

View file

@ -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

View file

@ -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)

View file

@ -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/

View file

@ -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)

View file

@ -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:

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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"
}

View file

@ -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,

View file

@ -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')"

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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

View file

@ -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())

View file

@ -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")

View file

@ -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()
}

View file

@ -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(),

View file

@ -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
}

View file

@ -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
}

View file

@ -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())
}

View file

@ -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.

View file

@ -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) {

View file

@ -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",
},

View file

@ -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()
}

View file

@ -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()
}

View file

@ -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"`

View file

@ -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()
}

View file

@ -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) {

View file

@ -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))
}

View file

@ -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())

View file

@ -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
}

View file

@ -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

View file

@ -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
}

View file

@ -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
}

View file

@ -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
View 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
}

View 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
View 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

View file

@ -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)
}

View file

@ -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)
}

View file

@ -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)
})

View 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
}

View 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
}

View file

@ -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:

View file

@ -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 {

View file

@ -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
}

View file

@ -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
}

View file

@ -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

View file

@ -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
}

View file

@ -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) {

View file

@ -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
}